21-13 策略权限守卫:初步完成核心逻辑&准备测试数据
创建全局共享模块
模块创建与配置
1. 模块创建命令详解
使用Nest CLI创建全局共享模块的命令:
nest g res modules/shared --no-spec
bash
nest g res
:快速生成资源模块(包含controller/service/module)modules/shared
:指定模块路径为modules/shared
--no-spec
:跳过测试文件生成
2. 全局模块配置强化
@Global()
@Module({
imports: [PrismaModule], // 导入Prisma等依赖模块
providers: [SharedService],
exports: [SharedService]
})
export class SharedModule {}
typescript
最佳实践:
- 将
SharedModule
导入到AppModule
的imports
数组中 - 全局服务应避免保存状态(保持无状态设计)
- 使用
forwardRef()
解决循环依赖问题
实际案例:
// app.module.ts
@Module({
imports: [forwardRef(() => SharedModule)]
})
export class AppModule {}
typescript
💡 全局模块的providers在整个应用生命周期中只会被实例化一次
主体构造器实现进阶
1. 动态模型查询增强版
async getSubject<T>(
subject: keyof PrismaClient,
user: User,
options?: {
where?: Prisma.WhereInput;
include?: Prisma.IncludeInput;
}
): Promise<T> {
return this.prisma[subject].findUnique({
where: { id: user.id, ...options?.where },
include: options?.include
}) as Promise<T>;
}
typescript
2. 实现要点详解
特性 | 说明 | 示例 |
---|---|---|
泛型支持 | 提供类型安全返回值 | getSubject<User>('user', currentUser) |
Prisma类型约束 | 限制subject为有效模型名 | keyof PrismaClient |
扩展查询条件 | 支持复杂查询场景 | { where: { status: 'ACTIVE' } } |
关联查询 | 支持include预加载 | { include: { posts: true } } |
3. 安全增强措施
// 安全校验中间件
private validateSubject(subject: string) {
const validModels = Object.keys(this.prisma);
if (!validModels.includes(subject)) {
throw new BadRequestException(`Invalid model: ${subject}`);
}
}
typescript
4. 性能优化建议
- 添加缓存层(Redis/MemoryCache)
- 实现批量查询方法
async getSubjects(subjects: string[], user: User) {
return Promise.all(
subjects.map(subject => this.getSubject(subject, user))
}
typescript
💡 在GraphQL场景中,可结合DataLoader实现高效数据加载
扩展知识:Prisma高级技巧
1. 动态模型类型推导
type ModelDelegate = {
[K in keyof PrismaClient]:
PrismaClient[K] extends { findUnique: any } ? K : never
}[keyof PrismaClient];
typescript
2. 事务支持
async getWithTransaction(
subject: ModelDelegate,
userId: string,
tx: Prisma.TransactionClient
) {
return tx[subject].findUnique({ where: { id: userId } });
}
typescript
3. 错误处理最佳实践
try {
return await this.getSubject(...);
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
throw new ConflictException('Data conflict');
}
throw e;
}
typescript
常见问题解答
Q:为什么全局服务要设计为无状态? A:避免多请求间的状态污染,确保线程安全
Q:动态模型访问如何避免SQL注入?
A:通过Prisma的参数化查询机制,且我们的validateSubject
方法已做白名单校验
Q:什么时候应该使用全局模块? A:当服务需要被多个模块频繁使用时(如:数据库访问、工具类服务)
延伸学习资源
通过这样的扩展实现,你的共享模块将具备:
- 完善的类型安全
- 灵活的查询能力
- 健壮的错误处理
- 良好的性能表现
权限守卫核心逻辑深度解析
双层循环校验算法优化版
算法流程图增强说明
性能优化技巧
- 短路优化:发现匹配立即跳出内层循环
- 预过滤:先检查必选权限是否存在
const requiredActions = new Set(policies.map(p => p.action));
if (!userAbilities.some(a => requiredActions.has(a.action))) {
return false; // 快速失败
}
typescript
- 缓存策略:对用户能力建立索引
const abilityMap = new Map();
userAbilities.forEach(ability => {
const key = `${ability.action}_${ability.subject}`;
abilityMap.set(key, ability);
});
typescript
字段级权限处理增强
多层级字段权限检查
private checkFieldAccess(ability, policy) {
// 处理嵌套字段权限 (如: 'profile.address.city')
if (policy.fields) {
return policy.fields.every(field => {
const fieldPath = field.split('.');
return fieldPath.every(segment =>
ability.can(policy.action, policy.subject, segment)
);
});
}
return ability.can(policy.action, policy.subject);
}
typescript
字段权限缓存方案
const fieldCache = new WeakMap();
function getFieldPermissionCache(ability, action, subject) {
const key = { action, subject };
if (!fieldCache.has(key)) {
fieldCache.set(key, new Set(
ability.rules
.filter(r => r.action === action && r.subject === subject)
.flatMap(r => r.fields || [])
));
}
return fieldCache.get(key);
}
typescript
白名单优化策略进阶
动态白名单配置
// 支持多种白名单类型
enum WhitelistType {
PATH = 'path',
ROLE = 'role',
IP = 'ip'
}
interface WhitelistRule {
type: WhitelistType;
value: string | RegExp;
}
// 配置示例
const whitelist: WhitelistRule[] = [
{ type: WhitelistType.PATH, value: '/api/health' },
{ type: WhitelistType.ROLE, value: 'SUPER_ADMIN' }
];
typescript
白名单检查服务
class WhitelistService {
private rules: WhitelistRule[];
constructor(rules: WhitelistRule[]) {
this.rules = rules;
}
isWhitelisted(request: Request): boolean {
return this.rules.some(rule => {
switch (rule.type) {
case WhitelistType.PATH:
return request.path.match(rule.value);
case WhitelistType.ROLE:
return request.user.roles.includes(rule.value);
case WhitelistType.IP:
return request.ip === rule.value;
}
});
}
}
typescript
权限守卫最佳实践
1. 调试日志集成
private logPermissionCheck(policy, granted) {
logger.debug(`Permission check:
Action: ${policy.action}
Subject: ${policy.subject}
Fields: ${policy.fields?.join(',')}
Result: ${granted}`);
}
typescript
2. 性能监控指标
const permissionCheckHistogram = new client.Histogram({
name: 'permission_check_duration_seconds',
help: 'Duration of permission checks',
buckets: [0.1, 0.5, 1]
});
function trackPermissionCheck() {
const end = permissionCheckHistogram.startTimer();
return {
end: () => end({ method: request.method, path: request.path })
};
}
typescript
3. 单元测试建议
describe('PolicyGuard', () => {
it('should allow when all permissions match', async () => {
const mockAbility = createMockAbility([
{ action: 'read', subject: 'Post' }
]);
const guard = new PolicyGuard(mockAbility);
const canActivate = await guard.canActivate(
createMockContext({
handler: {
metadata: {
get: () => [{ action: 'read', subject: 'Post' }]
}
}
})
);
expect(canActivate).toBe(true);
});
});
typescript
扩展知识:权限模型对比
模型类型 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
RBAC | 简单易用 | 灵活性低 | 固定角色系统 |
ABAC | 高度灵活 | 实现复杂 | 需要细粒度控制 |
ReBAC | 关系感知 | 性能挑战 | 社交网络系统 |
常见问题解决方案
问题1:权限检查性能瓶颈
- 解决方案:实现能力缓存,预编译权限规则
问题2:动态字段权限管理
- 解决方案:使用字段掩码技术
function applyFieldMask(data: object, allowedFields: string[]) {
return Object.fromEntries(
Object.entries(data).filter(([key]) => allowedFields.includes(key))
);
}
typescript
问题3:权限继承需求
- 解决方案:实现权限合并策略
function mergeAbilities(base: Ability[], extra: Ability[]) {
return [...base, ...extra].reduce((merged, current) => {
const existing = merged.find(a =>
a.action === current.action && a.subject === current.subject);
if (!existing) return [...merged, current];
return merged;
}, []);
}
typescript
延伸学习资源
测试数据准备:完整规范与最佳实践
数据清理规范增强版
1. 多环境数据清理策略
环境 | 清理策略 | 保留数据 | 工具推荐 |
---|---|---|---|
开发环境 | 全量清理 | 基础种子数据 | Prisma migrate reset |
测试环境 | 按需清理 | 测试用例数据 | 自定义清理脚本 |
CI环境 | 每次构建清理 | 空数据库 | Docker容器重建 |
2. 自动化清理脚本示例
// cleanup.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function cleanDatabase() {
const tablenames = await prisma.$queryRaw<
Array<{ tablename: string }>
>`SELECT tablename FROM pg_tables WHERE schemaname='public'`;
for (const { tablename } of tablenames) {
if (tablename !== '_prisma_migrations') {
try {
await prisma.$executeRawUnsafe(
`TRUNCATE TABLE "public"."${tablename}" CASCADE;`
);
} catch (error) {
console.log({ error });
}
}
}
}
typescript
3. 事务性清理流程
脏数据预防机制强化
1. 增强型DTO校验
export class CreatePolicyDto {
@IsString()
@Matches(/^[a-z_]+$/, {
message: 'Action只能包含小写字母和下划线'
})
action: string;
@IsValidSubject()
@ApiProperty({
example: 'Post',
description: 'Prisma模型名称',
enum: ['User', 'Post', 'Comment']
})
subject: string;
@IsOptional()
@ArrayNotEmpty()
@ArrayUnique()
@IsString({ each: true })
@MaxLength(20, { each: true })
fields?: string[];
}
typescript
2. 数据库层防护
model Policy {
id String @id @default(uuid())
action String @db.VarChar(50)
subject String @db.VarChar(50)
fields String[] @db.Json
@@unique([action, subject])
@@index([subject])
}
prisma
3. 业务逻辑校验
async createPolicy(dto: CreatePolicyDto) {
// 检查subject是否为有效模型
if (!Object.keys(this.prisma).includes(dto.subject)) {
throw new BadRequestException('无效的数据模型');
}
// 检查权限是否已存在
const exists = await this.prisma.policy.findUnique({
where: {
action_subject: {
action: dto.action,
subject: dto.subject
}
}
});
if (exists) throw new ConflictException('权限策略已存在');
}
typescript
测试数据工厂模式
1. 测试数据构建器
class PolicyBuilder {
private data: Partial<Policy> = {
action: 'read',
subject: 'Post'
};
withAction(action: string): this {
this.data.action = action;
return this;
}
build(): CreatePolicyDto {
return plainToInstance(CreatePolicyDto, this.data);
}
}
// 使用示例
const invalidPolicy = new PolicyBuilder()
.withAction('invalid action!')
.build();
typescript
2. 测试数据集管理
// test-data.ts
export const VALID_POLICIES = [
{ action: 'create', subject: 'Post' },
{ action: 'update', subject: 'Post', fields: ['title'] }
];
export const INVALID_POLICIES = [
{ action: '', subject: 'Post' }, // 空action
{ action: 'delete', subject: 'NonExistModel' }
];
typescript
测试覆盖率保障
1. 边界测试用例设计
测试类型 | 测试用例示例 | 预期结果 |
---|---|---|
正常流 | 有效action/subject组合 | 201 Created |
异常流 | 重复权限策略 | 409 Conflict |
边界值 | action长度为50字符 | 201 Created |
边界值 | action长度为51字符 | 400 Bad Request |
2. 自动化测试集成
describe('Policy API', () => {
beforeAll(async () => {
await cleanDatabase();
await seedTestData();
});
test.each(VALID_POLICIES)(
'应该成功创建策略 %j',
async (policy) => {
const res = await request(app)
.post('/policies')
.send(policy)
.expect(201);
expect(res.body).toMatchObject(policy);
}
);
});
typescript
数据质量监控方案
1. 定期数据健康检查
// 检查无效关联数据
async function checkOrphanedRecords() {
const orphans = await prisma.policyRole.findMany({
where: {
OR: [
{ policy: null },
{ role: null }
]
}
});
if (orphans.length > 0) {
alertAdmin(`发现${orphans.length}条无效关联数据`);
}
}
typescript
2. 数据校验中间件
async function validateDatabase() {
await checkForeignKeyIntegrity();
await checkDataFormatCompliance();
await checkBusinessRuleViolations();
}
// 定时任务
cron.schedule('0 3 * * *', validateDatabase);
typescript
延伸学习资源
通过这套完整的测试数据管理体系,可以确保:
✅ 测试环境数据一致性
✅ 自动化测试可靠性
✅ 生产数据质量可控性
✅ 团队协作效率提升
常见问题解决方案深度解析
类型错误处理进阶方案
1. 安全类型转换策略
// 安全类型转换函数
function safeFieldCast(field: unknown): string {
if (typeof field !== 'string') {
throw new TypeError(`Field must be string, got ${typeof field}`);
}
return field;
}
// 使用示例
ability.can(
policy.action,
policy.subject,
safeFieldCast(field) // 确保类型安全
);
typescript
2. 类型守卫模式
// 自定义类型守卫
function isStringArray(fields: unknown): fields is string[] {
return Array.isArray(fields) &&
fields.every(item => typeof item === 'string');
}
// 应用类型守卫
if (isStringArray(policy.fields)) {
policy.fields.forEach(field =>
ability.can(policy.action, policy.subject, field)
);
}
typescript
3. Schema验证集成
import { z } from 'zod';
const FieldSchema = z.string().min(1).max(50);
const PolicySchema = z.object({
action: z.string(),
subject: z.string(),
fields: z.array(FieldSchema).optional()
});
// 验证使用
const validated = PolicySchema.parse(policy);
validated.fields?.forEach(field =>
ability.can(validated.action, validated.subject, field)
);
typescript
空权限策略处理增强
1. 多层级空值检查
function shouldSkipAuth(policies?: Policy[]): boolean {
// 检查undefined/null/空数组
return !policies || policies.length === 0;
}
// 使用位置
if (shouldSkipAuth(policies)) {
return true;
}
typescript
2. 元数据标记模式
// 使用装饰器标记无需验证的接口
@Controller('public')
@SkipAuth()
export class PublicController {
@Get()
getPublicData() {
return { data: 'public' };
}
}
typescript
3. 权限白名单配置
# auth-whitelist.yaml
skipAuthRoutes:
- GET /health
- GET /public/*
- POST /auth/login
yaml
循环优化高级技巧
1. 短路迭代模式
// 使用some替代forEach+break
const hasPermission = abilities.some(ability =>
ability.can(policy.action, policy.subject)
);
if (hasPermission) {
// 处理通过逻辑
}
typescript
2. 能力索引预构建
// 建立能力索引
const abilityIndex = new Map<string, Ability>();
abilities.forEach(ability => {
const key = `${ability.action}:${ability.subject}`;
abilityIndex.set(key, ability);
});
// 快速查找
const ability = abilityIndex.get(`${action}:${subject}`);
if (ability?.can(action, subject)) {
// 权限通过
}
typescript
3. 并行检查优化
// 使用Promise.all进行并行检查
const checks = policies.map(policy =>
checkSinglePolicy(abilities, policy)
);
const results = await Promise.all(checks);
const allPassed = results.every(Boolean);
typescript
性能对比实验数据
方法 | 1000次迭代耗时(ms) | 内存占用(MB) |
---|---|---|
基础循环 | 125 | 45 |
some短路 | 68 | 42 |
预构建索引 | 32 | 55 |
并行检查 | 89 | 60 |
错误处理最佳实践
1. 调试日志集成
function checkPermission(ability, policy) {
const result = ability.can(policy.action, policy.subject);
logger.debug(`Permission check:
Action: ${policy.action}
Subject: ${policy.subject}
Result: ${result}`);
return result;
}
typescript
2. 性能监控埋点
const permissionCheckHistogram = new Prometheus.Histogram({
name: 'permission_check_duration_seconds',
help: 'Permission check latency',
labelNames: ['action', 'subject'],
buckets: [0.1, 0.5, 1]
});
function withMonitoring(fn) {
return (ability, policy) => {
const end = permissionCheckHistogram.startTimer();
try {
return fn(ability, policy);
} finally {
end({
action: policy.action,
subject: policy.subject
});
}
};
}
typescript
扩展应用场景
1. 前端权限指令
// Angular指令示例
@Directive({ selector: '[hasPermission]' })
export class HasPermissionDirective {
@Input() hasPermission: string;
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef,
private auth: AuthService
) {
this.auth.abilities$.subscribe(() => this.updateView());
}
private updateView() {
const hasAccess = this.auth.can(this.hasPermission);
this.viewContainer.clear();
if (hasAccess) {
this.viewContainer.createEmbeddedView(this.templateRef);
}
}
}
typescript
2. 单元测试覆盖方案
describe('PermissionService', () => {
let service: PermissionService;
beforeEach(() => {
service = new PermissionService();
});
test.each([
{ policies: [], expected: true },
{ policies: undefined, expected: true },
{ policies: [{ action: 'read', subject: 'Post' }], expected: false }
])('should handle empty policies %j', ({ policies, expected }) => {
expect(service.checkPolicies(policies)).toBe(expected);
});
});
typescript
延伸学习资源
通过这套完整的解决方案,可以实现:
✅ 类型安全的权限检查
✅ 高效的空策略处理
✅ 最优化的循环性能
✅ 可观测的权限系统
↑